﻿/**
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, 
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished
 * to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Collections.Specialized;
using System.Xml;
using System.Xml.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.IO;
using QDrive.log4net;

namespace QDrive.QBClient
{
    public class QuickBaseClient
    {
        public const int MILLIS_IN_MINUTE = 60000;
        public const int MAX_TIMEOUT = 6 * MILLIS_IN_MINUTE; // 6 minute timeout, loading some apps take a significant amount of time...

        public bool traceEnabled { get; set; }

        // Fields
        private ILog logger = LogManager.GetLogger(typeof(QuickBaseClient));
        public string _ticket;
        public readonly UriBuilder _uri;
        private string username;
        private string password;
        public string _domain;

        // Methods
        private QuickBaseClient() { }

        public String getTicket()
        {
            return _ticket;
        }

        public QuickBaseClient(string hostname, string username, string password)
        {
            if (string.IsNullOrEmpty(username))
                throw new ArgumentException("Username is null or empty");

            if (string.IsNullOrEmpty(password))
                throw new ArgumentException("Password is null or empty");

            if (string.IsNullOrEmpty(hostname))
                throw new ArgumentException("Hostname is null or empty");
            this._domain = hostname;
            this._uri = new UriBuilder("https", hostname, 0x1bb);
            this.SelectDbid("main");
            this.username = username;
            this.password = password;
        }

        public XDocument listAll(String dbid)
        {
            SelectDbid(dbid);
            NameValueCollection values = new NameValueCollection();
            values.Add("query", "{1.XEX.''}");
            XDocument xmlResponse = this.PostNameValueCollection("API_DoQuery", values);
            String body = xmlResponse.ToString();
            return xmlResponse;
        }

        public XDocument getGrantedApps()
        {
            XDocument xmlResponse = GetMainHandler("API_GrantedDBs&withembeddedtables=0");
            String body = xmlResponse.ToString();
            return xmlResponse;
        }

        public XDocument getSchema(String appId, String domain = null)
        {
            SelectDbid(appId);
            NameValueCollection nvc = new NameValueCollection();
            if (null != domain)
            {
                nvc.Add("realmhost", domain);
            }

            XDocument xmlResponse = this.PostNameValueCollection("API_GetSchema", nvc);
            String body = xmlResponse.ToString();
            return xmlResponse;
        }

        public void Authenticate(bool retry = true)
        {
            NameValueCollection values2 = new NameValueCollection();
            values2.Add("username", username);
            values2.Add("password", password);
            NameValueCollection n = values2;
            XDocument document = this.PostNameValueCollection("API_Authenticate", n, false);
            this._ticket = document.Descendants().Where<XElement>(delegate(XElement p)
            {
                return ((p.NodeType == XmlNodeType.Element) && (p.Name.LocalName == "ticket"));
            }).Select<XElement, string>(delegate(XElement p)
            {
                return p.Value;
            }).FirstOrDefault<string>();
            // auth failure?
            if (string.IsNullOrEmpty(this._ticket))
            {
                throw new QuickbaseApiException(-1, "API_Authenticate did not return ticket.", "", "API_Authenticate", null);
            }
        }

        public XDocument PostNameValueCollection(string act, NameValueCollection n, bool retry = true)
        {
            XDocument xmlRequest = this.StubRequest();
            if (logger.IsDebugEnabled())
            {
                logger.Debug("Sending request: " + xmlRequest.ToString());
            }
            foreach (string str in n)
            {
                xmlRequest.Element("qdbapi").Add(new XElement(str, n[str]));
            }
            return this.PostXml(act, xmlRequest, retry);
        }

        private XDocument PostXml(string act, XDocument xmlRequest, bool retry = true)
        {
            int retryCount = 0;
            WebException exception;
            do
            {
                try
                {
                    return PostAPIURL(act, xmlRequest.ToString());
                }
                catch (WebException e)
                {
                    if (System.Net.WebExceptionStatus.Timeout == e.Status && retry)
                    {
                        retryCount++;
                        logger.Warn("WebException retrieved attempting to send data to QuickBase, retry (# " + retryCount + ")", e);
                        exception = e;
                        // do not retry authentication in this loop
                        Authenticate(false);
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
            while (retryCount < 3 && retry);
            throw exception; // give up
        }

        public void SelectDbid(string dbid)
        {
            this._uri.Path = "/db/" + dbid;
        }

        private XDocument StubRequest()
        {
            return new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new object[] { new XElement("qdbapi", new XElement("ticket", this._ticket)) });
        }

        private static bool ValidateRemoteCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors)
        {
            return true;
        }

        public String ExportAsCSV(string dbid)
        {
            return "";
        }

        public void ImportFromCSV(string dbId, string csvData, string clist, bool skipfirst)
        {
            StringBuilder sb = new StringBuilder("<qdbapi><clist>");
            sb.Append(clist);
            sb.Append("</clist>");
            sb.Append("<records_csv><![CDATA[");
            sb.Append(csvData);
            sb.Append("]]></records_csv>");
            if (skipfirst)
            {
                sb.Append("<skipfirst>1</skipfirst>");
            }
            sb.Append("</qdbapi>");
            PostAPIURL(dbId, "API_ImportFromCSV", sb.ToString());
        }

        public void ImportFromCSV(string dbId, string[] csvData, int offset, int count, string clist, bool skipfirst)
        {
            StringBuilder sb = new StringBuilder("<qdbapi><clist>");
            sb.Append(clist).Append("</clist>").Append("<records_csv><![CDATA[");
            for (int i = offset; i < count && i < csvData.Length; ++i)
            {
                sb.Append(csvData[i] + "\n");
            }
            sb.Append("]]></records_csv>");
            if (skipfirst)
            {
                sb.Append("<skipfirst>1</skipfirst>");
            }
            sb.Append("</qdbapi>");

            logger.Debug("API_ImportFromCSV Data\n " + sb.ToString());

            PostAPIURL(dbId, "API_ImportFromCSV", sb.ToString());
        }


        public XDocument PostAPIURL(string dbId, string action, string content)
        {
            SelectDbid(dbId);
            return PostAPIURL(action, content);
        }

        public void SelectMainHandler()
        {
            this.SelectDbid("main");
        }

        private XDocument GetMainHandler(string action)
        {
            SelectMainHandler();
            return GetHandler(this._uri.Uri.ToString(), action);
        }

        public String GetHandlerAsString(string uri)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this._uri + uri);
            request.Timeout = MAX_TIMEOUT;
            request.Method = "GET";
            ServicePointManager.ServerCertificateValidationCallback = (RemoteCertificateValidationCallback)Delegate.Combine(ServicePointManager.ServerCertificateValidationCallback, new RemoteCertificateValidationCallback(QuickBaseClient.ValidateRemoteCertificate));
            if (!string.IsNullOrEmpty(_ticket))
            {
                if (request.CookieContainer == null)
                {
                    CookieContainer c = new CookieContainer();
                    Cookie cookie = new Cookie("Ticket", _ticket);
                    cookie.Domain = this._uri.Uri.Host;
                    c.Add(cookie);
                    request.CookieContainer = c;
                }
            }
            WebResponse response = request.GetResponse();
            HttpStatusCode statusCode = ((HttpWebResponse)response).StatusCode;
            if (statusCode == HttpStatusCode.OK)
            {
                TextReader textReader = new StreamReader(response.GetResponseStream());
                StringBuilder sb = new StringBuilder();
                string line;
                do
                {
                    line = textReader.ReadLine();
                    if (null == line) 
                        break;
                    sb.Append(line).Append("\n");
                }
                while (true);
                response.Close();
                return sb.ToString();
            }
            response.Close();

            return null;
        }

        private XDocument GetHandler(string uri, string action)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri + "?a=" + action);
            request.Timeout = MAX_TIMEOUT;
            request.Method = "GET";
            ServicePointManager.ServerCertificateValidationCallback = (RemoteCertificateValidationCallback)Delegate.Combine(ServicePointManager.ServerCertificateValidationCallback, new RemoteCertificateValidationCallback(QuickBaseClient.ValidateRemoteCertificate));
            if (!string.IsNullOrEmpty(_ticket))
            {
                if (request.CookieContainer == null)
                {
                    CookieContainer c = new CookieContainer();
                    Cookie cookie = new Cookie("Ticket", _ticket);
                    cookie.Domain = this._uri.Uri.Host;
                    c.Add(cookie);
                    request.CookieContainer = c;
                }
            }
            WebResponse response = request.GetResponse();
            HttpStatusCode statusCode = ((HttpWebResponse)response).StatusCode;
            XDocument xmlResponse = new XDocument();
            QuickbaseApiException apiException = null;
            if (statusCode == HttpStatusCode.OK)
            {
                TextReader textReader = new StreamReader(response.GetResponseStream());
                xmlResponse = XDocument.Load(textReader);
                apiException = QuickbaseApiException.Create("Get Request", xmlResponse, action, this._uri.Uri);
            }
            response.Close();

            if (logger.IsDebugEnabled())
            {
                logger.Debug("QB Response action=" + action + ", body=" + xmlResponse.ToString());
            }
            if (statusCode != HttpStatusCode.OK)
                throw new QuickbaseHttpStatusCodeException(statusCode);
            if (apiException != null)
                throw apiException;
            return xmlResponse;
        }

        private XDocument PostAPIURL(string action, string content)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this._uri.Uri);
            request.Timeout = MAX_TIMEOUT;
            request.Method = "POST";
            ServicePointManager.ServerCertificateValidationCallback = (RemoteCertificateValidationCallback)Delegate.Combine(ServicePointManager.ServerCertificateValidationCallback, new RemoteCertificateValidationCallback(QuickBaseClient.ValidateRemoteCertificate));
            request.ContentType = "application/xml";
            request.Headers.Add("QUICKBASE-ACTION", action);
            if (!string.IsNullOrEmpty(_ticket))
            {
                if (request.CookieContainer == null)
                {
                    CookieContainer c = new CookieContainer();
                    Cookie cookie = new Cookie("Ticket", _ticket);
                    cookie.Domain = this._uri.Uri.Host;
                    c.Add(cookie);
                    request.CookieContainer = c;
                }
            }

            byte[] bytes = Encoding.UTF8.GetBytes(content);
            request.ContentLength = bytes.Length;
            request.GetRequestStream().Write(bytes, 0, bytes.Length);
            request.GetRequestStream().Flush();
            WebResponse response = request.GetResponse();
            HttpStatusCode statusCode = ((HttpWebResponse)response).StatusCode;
            XDocument xmlResponse = new XDocument();
            QuickbaseApiException apiException = null;
            if (statusCode == HttpStatusCode.OK)
            {
                TextReader textReader = new StreamReader(response.GetResponseStream());
                xmlResponse = XDocument.Load(textReader);
                apiException = QuickbaseApiException.Create(content, xmlResponse, action, this._uri.Uri);
            }
            request.GetRequestStream().Close();
            response.Close();

            if (logger.IsDebugEnabled())
            {
                logger.Debug("QB Response action=" + action + ", body=" + xmlResponse.ToString());
            }
            if (statusCode != HttpStatusCode.OK)
                throw new QuickbaseHttpStatusCodeException(statusCode);
            if (apiException != null)
                throw apiException;
            return xmlResponse;
        }


        public byte[] GetHandlerAsByteArray(string uri)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://" + _domain + uri);
            request.Timeout = MAX_TIMEOUT;
            request.Method = "GET";
            ServicePointManager.ServerCertificateValidationCallback = (RemoteCertificateValidationCallback)Delegate.Combine(ServicePointManager.ServerCertificateValidationCallback, new RemoteCertificateValidationCallback(QuickBaseClient.ValidateRemoteCertificate));
            if (!string.IsNullOrEmpty(_ticket))
            {
                if (request.CookieContainer == null)
                {
                    CookieContainer c = new CookieContainer();
                    Cookie cookie = new Cookie("Ticket", _ticket);
                    cookie.Domain = this._uri.Uri.Host;
                    c.Add(cookie);
                    request.CookieContainer = c;
                }
            }
            WebResponse response = request.GetResponse();
            HttpStatusCode statusCode = ((HttpWebResponse)response).StatusCode;
            if (statusCode == HttpStatusCode.OK)
            {
                byte[] buffer = new byte[4096];
                BinaryReader sr = new BinaryReader(response.GetResponseStream());
                MemoryStream ms = new MemoryStream();
                int len = 0;
                do 
                {
                    len = sr.Read(buffer, 0, buffer.Length);
                    ms.Write(buffer, 0, len);
                }
                while (len > 0);
                byte[] outBuf = new byte[ms.Length];
                ms.Seek(0, SeekOrigin.Begin);
                ms.Read(outBuf, 0, outBuf.Length);
                ms.Close();
                return outBuf;
            }
            response.Close();

            return null;
        }

        public long GetHeadSize(string uri)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://" + _domain + uri);
            request.Timeout = MAX_TIMEOUT;
            request.Method = "HEAD";
            ServicePointManager.ServerCertificateValidationCallback = (RemoteCertificateValidationCallback)Delegate.Combine(ServicePointManager.ServerCertificateValidationCallback, new RemoteCertificateValidationCallback(QuickBaseClient.ValidateRemoteCertificate));
            if (!string.IsNullOrEmpty(_ticket))
            {
                if (request.CookieContainer == null)
                {
                    CookieContainer c = new CookieContainer();
                    Cookie cookie = new Cookie("Ticket", _ticket);
                    cookie.Domain = this._uri.Uri.Host;
                    c.Add(cookie);
                    request.CookieContainer = c;
                }
            }
            WebResponse response = request.GetResponse();
            string val = response.Headers["Content-Length"];
            response.Close();
            return Int64.Parse(val);
        }
        
    }
}
